[TDR Generic表][C++ SDK]TDR Blob内嵌Protobuf部分字段读写

混合字段路径的综合读写示例、路径语法及限制见《TDR Blob 内嵌 Protobuf 部分字段读写》。本文介绍 C++ 的 TdrPbFieldGroupSetAllTdrPbFieldNames,以及完整的 Service API 请求与响应示例。

1. 接口说明

TDR 表的 Blob 字段(char 数组 + refer 长度字段)里如果保存的是 Protobuf 序列化数据, 可以只读取或只更新 Blob 中指定的 PB 字段,而不必整段回读、改完再整段回写。

三个命令对应三个命令字:

命令 命令字 说明
FieldGet TCAPLUS_API_PB_FIELD_GET_REQ 0x0067 读取 Blob 中指定的 PB 字段
FieldSet TCAPLUS_API_PB_FIELD_SET_REQ 0x0069 更新 Blob 中指定的 PB 字段,记录不存在会报错,不会自动插入
BatchFieldGet TCAPLUS_API_PB_BATCH_FIELD_GET_REQ 0x0075 多个主键共用一组字段路径批量读取

Blob 中 Protobuf 字段的自增(TCAPLUS_API_PB_FIELD_INCREASE_REQ)在 TDR 表上不支持。

本特性的字段名适配层是可选头文件 tcaplus_tdr_pb_fields.h,只替换这一行:

request->SetFieldNames(field_names, field_count);

其余请求和响应流程保持 Service API 原有用法不变。不 include 该头文件的 TDR 用户不会被强制依赖 protobuf。

2. 版本要求

  • Service API 3.55.0 及以上;
  • 服务端需要支持该特性。未适配的服务端在这三个命令上会直接返回找不到 PB 描述的错误,使用前请先确认服务端版本。

3. 准备工作

参见准备工作文档,完成使用该接口前的准备工作,并创建 TDR Generic 表。

本特性要求表里有一个 Blob 字段 + 它的 refer 长度字段。下面以 RoleSummaryTable 为例:

<struct name="Col" version="1">
    <entry name="len"   type="uint" defaultvalue="0" desc="bin 的有效长度" />
    <entry name="bin"   type="char" count="1024" refer="len" desc="PB 数据" />
</struct>

<struct name="RoleSummaryTable" version="1" primarykey="id" splittablekey="id">
    <entry name="id"    type="int64" desc="角色ID" />
    <entry name="del"   type="int"   desc="删除标记" />
    <entry name="col1"  type="Col"   desc="保存 ProfileBaseData 的 Blob" />
</struct>

col1.bin 是保存 PB 的 Blob 字段,col1.len 是它的 refer 长度字段。 注意区分两套名字:字段路径里用 TDR 表定义的字段名col1.bincol1.lendel), C++ 代码里用 tdr 生成的成员名stCol1.szBinstCol1.dwLeniDelllId)。

Blob 里的 PB 定义示例(ProfileBaseData,不是 Tcaplus PB 表,只是普通 proto,不需要 tcaplus 的 proto 选项):

syntax = "proto3";
package tcaplus_demo;

message ProfileBaseData {
    uint32 level = 5;
    string plat_name = 6;
    repeated uint32 equipped_tag_list = 16;
    map<int32, BanInfo> ban_info_map = 22;
    map<string, string> settings = 23;
}
message BanInfo { string info = 1; int32 duration = 2; }

完整示例见 examples/tcaplus/C++_tdr2.0_tdr_pb_fields:

内容 路径
表定义 examples/tcaplus/C++_tdr2.0_tdr_pb_fields/table_test.xml
PB 定义 examples/tcaplus/C++_tdr2.0_tdr_pb_fields/profile_base_data.proto
示例代码 examples/tcaplus/C++_tdr2.0_tdr_pb_fields/main.cpp
编译与运行 examples/tcaplus/C++_tdr2.0_tdr_pb_fields/readme.txt

示例里的三个函数分别对应下面 5.1、5.2、5.3,响应处理对应 5.4。

4. 字段路径

部分字段读写的核心是字段路径,服务端没有业务的 .proto,只认数字 tag 路径:

PB 定义 业务写法 字段路径
uint32 level = 5 level 5
string plat_name = 6 plat_name 6
repeated uint32 equipped_tag_list = 16 equipped_tag_list 16(proto3 默认 packed,整组读写)
map<int32, BanInfo> ban_info_map = 22 ban_info_map[1001] 22[1001]
同上 ban_info_map[1001].info 22[1001].1
map<string, string> settings = 23 settings['region'] 23['region']

加上 Blob 字段名前缀后得到最终路径:col1.bin.5col1.bin.22[1001].1

要点:

  • 承载路径的一级字段和 refer 长度字段由 SDK 自动补齐,业务不需要把 col1.bincol1.len 写进字段名列表;
  • 原生 TDR 字段直接写字段名,例如 del
  • 删除 map 元素在路径上加 POP 前缀(POP col1.bin.22[1001]);
  • 一条路径最多一层容器元素访问;packed 的标量数组只能整组读写,不支持下标。

字段名到 tag 路径的转换用 tcaplus_tdr_pb_fields.h 里的适配层:

#include "tcaplus_tdr_pb_fields.h"

const char* col1_fields[] = {
    "level",
    "plat_name",
    "ban_info_map[1001]"
};

const TcaplusService::TdrPbFieldGroup field_groups[] = {
    TcaplusService::TdrPbFieldGroup(
        "col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields),
    TcaplusService::TdrPbFieldGroup("del")   // 原生 TDR 字段,原样透传
};

ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);

不用适配层也可以,直接写数字 tag 路径:

const char* field_names[] = {"col1.bin.5", "col1.bin.6", "del"};
ret = request->SetFieldNames(field_names, 3);

4.1 TdrPbFieldGroup

表示一个原生 TDR 字段,或者同一个 Blob 中的一组 PB 字段。

// 原生 TDR 字段,路径不进入 PB 解析器,原样传给 SetFieldNames()
explicit TdrPbFieldGroup(const char* tdr_field_path);

// 显式的"数组指针 + 数量"形式。descriptor 为 NULL 表示调用方只提供数字 tag
TdrPbFieldGroup(
    const char* tdr_blob_prefix,
    const ::google::protobuf::Descriptor* descriptor,
    const char* const pb_field_names[],
    unsigned pb_field_count);

// 普通数组形式,自动推导字段数量
template <size_t N>
TdrPbFieldGroup(
    const char* tdr_blob_prefix,
    const ::google::protobuf::Descriptor* descriptor,
    const char* const (&pb_field_names)[N]);

4.2 SetAllTdrPbFieldNames()

int32_t SetAllTdrPbFieldNames(
    TcaplusServiceRequest* request,
    const TdrPbFieldGroup field_groups[],
    unsigned field_group_count);

template <size_t N>
int32_t SetAllTdrPbFieldNames(
    TcaplusServiceRequest* request,
    const TdrPbFieldGroup (&field_groups)[N]);

数组模板只是省去手写 sizeof(array) / sizeof(array[0])。适配层先把全部结果保存到临时容器, 确认所有分组都转换成功后,才调用一次 request->SetFieldNames(),本地转换失败不会给 request 留下半组字段。

5. 示例代码

Blob 里必须是合法的 PB 编码,普通写入的裸字符串服务端解析不了,先整段写一份完整 PB。

示例代码见 examples/tcaplus/C++_tdr2.0_tdr_pb_fields/main.cpp,对应函数为 SendProfileFieldSetSendProfileFieldGetSendProfileBatchFieldGetHandleProfileFieldResponse,入口在 main() 中。

5.1 FieldSet(部分更新)

int32_t SendProfileFieldSet(TcaplusService::TcaplusServer& server, int64_t role_id)
{
    const char kTableName[] = "RoleSummaryTable";

    // 1. 创建并初始化原生 Service API request
    TcaplusService::TcaplusServiceRequest* request = server.GetRequest(kTableName);
    if (request == NULL)
    {
        return TcapErrCode::API_ERR_PARAMETER_INVALID;
    }

    int32_t ret = request->Init(TcaplusService::TCAPLUS_API_PB_FIELD_SET_REQ);
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    // 2. 业务构造增量 PB,并直接序列化到 TDR Blob
    tcaplus_demo::ProfileBaseData delta;
    delta.set_level(100);
    delta.set_plat_name("plat_x");
    (*delta.mutable_ban_info_map())[1001].set_info("ban info");

    ROLESUMMARYTABLE row;
    memset(&row, 0, sizeof(row));
    row.llId = role_id;   // tdr 生成的成员名,不是 xml 里的 id

    const size_t encoded_size = delta.ByteSizeLong();
    if (encoded_size > sizeof(row.stCol1.szBin))
    {
        return TcapErrCode::API_ERR_OVER_MAX_FIELD_VALUE_LEN;
    }
    if (!delta.SerializePartialToArray(row.stCol1.szBin, static_cast<int>(encoded_size)))
    {
        return TcapErrCode::API_ERR_PACK_MESSAGE;
    }
    row.stCol1.dwLen = static_cast<uint32_t>(encoded_size);

    // 3. 继续使用原生 record;TDR key、Blob 和其他字段都由业务控制
    TcaplusService::TcaplusServiceRecord* record = request->AddRecord();
    if (record == NULL)
    {
        return TcapErrCode::API_ERR_PARAMETER_INVALID;
    }
    ret = record->SetData(&row, sizeof(row));
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    // 4. 唯一被适配的步骤:按 PB 字段名设置部分字段集合
    const char* col1_fields[] = {
        "level",
        "plat_name",
        "ban_info_map[1001]"
    };
    const TcaplusService::TdrPbFieldGroup field_groups[] = {
        TcaplusService::TdrPbFieldGroup(
            "col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields)
    };

    ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    return server.SendRequest(request);
}

SetAllTdrPbFieldNames() 最终设置的是 col1.bin.5col1.bin.6col1.bin.22[1001], 只有这三个路径允许被 FieldSet 修改,Blob 中其余字段服务端原样保留。

5.2 FieldGet(部分读取,同时读原生 TDR 字段)

int32_t SendProfileFieldGet(TcaplusService::TcaplusServer& server, int64_t role_id)
{
    const char kTableName[] = "RoleSummaryTable";

    TcaplusService::TcaplusServiceRequest* request = server.GetRequest(kTableName);
    if (request == NULL)
    {
        return TcapErrCode::API_ERR_PARAMETER_INVALID;
    }

    int32_t ret = request->Init(TcaplusService::TCAPLUS_API_PB_FIELD_GET_REQ);
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    ROLESUMMARYTABLE row;
    memset(&row, 0, sizeof(row));
    row.llId = role_id;

    TcaplusService::TcaplusServiceRecord* record = request->AddRecord();
    if (record == NULL)
    {
        return TcapErrCode::API_ERR_PARAMETER_INVALID;
    }
    ret = record->SetData(&row, sizeof(row));
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    const char* col1_fields[] = {
        "level",
        "plat_name",
        "ban_info_map[1001].info",
        "settings['region']"
    };
    const TcaplusService::TdrPbFieldGroup field_groups[] = {
        TcaplusService::TdrPbFieldGroup(
            "col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields),
        TcaplusService::TdrPbFieldGroup("del")
    };

    ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    return server.SendRequest(request);
}

del 不经过 PB 解析,和四个 PB 路径一起传入同一次 SetFieldNames()

5.3 BatchFieldGet(批量读取)

int32_t SendProfileBatchFieldGet(
    TcaplusService::TcaplusServer& server,
    const int64_t role_ids[],
    unsigned role_count)
{
    const char kTableName[] = "RoleSummaryTable";

    TcaplusService::TcaplusServiceRequest* request = server.GetRequest(kTableName);
    if (request == NULL)
    {
        return TcapErrCode::API_ERR_PARAMETER_INVALID;
    }

    int32_t ret = request->Init(TcaplusService::TCAPLUS_API_PB_BATCH_FIELD_GET_REQ);
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    for (unsigned i = 0; i < role_count; ++i)
    {
        ROLESUMMARYTABLE row;
        memset(&row, 0, sizeof(row));
        row.llId = role_ids[i];

        TcaplusService::TcaplusServiceRecord* record = request->AddRecord();
        if (record == NULL)
        {
            return TcapErrCode::API_ERR_PARAMETER_INVALID;
        }

        ret = record->SetData(&row, sizeof(row));
        if (ret != TcapErrCode::GEN_ERR_SUC)
        {
            return ret;
        }
    }

    // 所有记录共用同一组字段路径,只设置一次
    const char* col1_fields[] = {"level", "plat_name"};
    const TcaplusService::TdrPbFieldGroup field_groups[] = {
        TcaplusService::TdrPbFieldGroup(
            "col1.bin", tcaplus_demo::ProfileBaseData::descriptor(), col1_fields)
    };

    ret = TcaplusService::SetAllTdrPbFieldNames(request, field_groups);
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    return server.SendRequest(request);
}

单次最多 1024 条主键。

5.4 响应处理

响应端不使用适配层,直接读取 TDR,再解析部分 PB:

int HandleProfileFieldResponse(TcaplusService::TcaplusServiceResponse* response)
{
    if (response == NULL)
    {
        return TcapErrCode::API_ERR_PARAMETER_INVALID;
    }

    int32_t ret = response->GetResult();
    if (ret != TcapErrCode::GEN_ERR_SUC)
    {
        return ret;
    }

    int32_t first_error = TcapErrCode::GEN_ERR_SUC;
    const int record_count = response->GetRecordCount();
    for (int i = 0; i < record_count; ++i)
    {
        const TcaplusService::TcaplusServiceRecord* record = NULL;
        ret = response->FetchRecord(record);
        if (ret != TcapErrCode::GEN_ERR_SUC)
        {
            if (first_error == TcapErrCode::GEN_ERR_SUC)
            {
                first_error = ret;
            }
            continue;
        }

        ROLESUMMARYTABLE row;
        memset(&row, 0, sizeof(row));
        ret = record->GetData(&row, sizeof(row));
        if (ret != TcapErrCode::GEN_ERR_SUC)
        {
            if (first_error == TcapErrCode::GEN_ERR_SUC)
            {
                first_error = ret;
            }
            continue;
        }

        if (row.stCol1.dwLen > sizeof(row.stCol1.szBin))
        {
            if (first_error == TcapErrCode::GEN_ERR_SUC)
            {
                first_error = TcapErrCode::API_ERR_OVER_MAX_FIELD_VALUE_LEN;
            }
            continue;
        }

        tcaplus_demo::ProfileBaseData profile;
        if (!profile.ParsePartialFromArray(row.stCol1.szBin, static_cast<int>(row.stCol1.dwLen)))
        {
            if (first_error == TcapErrCode::GEN_ERR_SUC)
            {
                first_error = TcapErrCode::API_ERR_UNPACK_MESSAGE;
            }
            continue;
        }

        // profile 只包含本次请求返回的部分字段,不能当作完整记录使用
        ConsumeProfile(row.llId, row.iDel, profile);
    }

    return first_error;
}

6. 适配层错误码

本地检查(SetAllTdrPbFieldNames() 返回值):

场景 返回值
request、数组、前缀或字段路径参数非法 API_ERR_PARAMETER_INVALID
descriptor 中不存在字段名或 tag API_ERR_FIELD_NOT_EXSIST
selector、map、repeated 或嵌套 message 类型不匹配 API_ERR_FIELD_TYPE_NOT_MATCH
request 未初始化、字段数过多、完整路径过长 原样返回 SetFieldNames() 的错误码

发送后的结果看 response->GetResult()。常见服务端错误码:

错误码 含义
TXHDB_ERR_RECORD_NOT_EXIST 记录不存在
COMMON_ERR_ELEMENT_NOT_EXIST 指定的 map key 或 repeated 下标不存在
COMMON_ERR_CONDITION_NOT_MATCHED 条件不成立
SVR_ERR_FAIL_INVALID_VERSION 版本校验失败

详见错误码含义和处理方法

7. 注意事项

  1. 只支持 Generic 表,List 表只能由服务端拒绝,SDK 本地拿不到表类型。
  2. Blob 里必须是当前业务 descriptor 对应的 PB 编码,历史数据的 schema 兼容性由业务保证。
  3. 返回的 col1.len本次部分 PB 的长度,不是记录中完整 Blob 的长度; 解析出的 PB 对象只包含本次请求的字段,不能直接用于整段 Blob 覆盖。
  4. SDK 不做 PB 编解码,增量 PB 的构造、Blob 的写入、响应 Blob 的解析都由业务负责, 适配层只解决 PB field name -> field number 的映射。
  5. 路径里的一级字段名取 TDR 表定义中的字段名,不是 C++ 结构体字段名。
  6. 字段名使用 .proto 中的原始 field name,不使用 JSON name。
  7. 头文件依赖完整 protobuf descriptor API,传入 NULL 只表示本次按数字 tag 转换; 不 include 该头文件的 TDR 用户不会被引入 protobuf 依赖。
  8. 嵌套结构体里的 Blob(如本例的 col1.bin)也支持,补齐的是嵌套结构体字段本身。

8. 其它参考文档

TDR Blob 内嵌 Protobuf 部分字段读写

[TDR Generic表][Go SDK]TDR Blob内嵌Protobuf部分字段读写

[TDR Generic表][C++ SDK]更新单条数据

[TDR Generic表][C++ SDK]条件过滤和更新说明

results matching ""

    No results matching ""